﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;

namespace EasyFlashCartBuilder
{
    class Program
    {
        static int Main(string[] args)
        {
            if (args.Length != 1)
            {
                Console.WriteLine("Usage:");
                Console.WriteLine("EasyFlashCartBuilder.exe Manifest.xml");
                return 1;
            }

            Console.WriteLine("EasyFlash Cart Builder, by King Durin");
            Console.WriteLine("http://efcb.codeplex.com");
            Console.WriteLine("Portions based on bin2efcrt by Skoe.");
            Console.WriteLine();

            var manifestFilename = args[0];

            if (!File.Exists(manifestFilename))
            {
                Console.WriteLine("Cannot load manifest file. Aborting.");
                return 1;
            }

            var bytesWritten = 0x0000l;
            var xmlDocument = XDocument.Load(manifestFilename);

            var efcartNode = xmlDocument.Descendants("efcart").FirstOrDefault();
            var cartName = (string)efcartNode.Attribute("name");
            var outFilename = (string) efcartNode.Attribute("outputfile");

            var bootNode = efcartNode.Descendants("boot").FirstOrDefault();
            var bootFilename = (string)bootNode.Attribute("filename");

            var binFile = File.Open(bootFilename, FileMode.Open);
            var binFileSize = binFile.Length;

            if (binFileSize != 16 * 1024)
            {
                Console.WriteLine("{0} is not 16k.", bootFilename);
                return 1;
            }

            var bytes = new byte[0xFFFF];

            var output = File.OpenWrite(outFilename);

            writeHeader(output, bytes, cartName);
            bytesWritten += 64;

            byte bank = 0;
            byte chip = 0;
            UInt32 remaining = (UInt32)binFileSize;
            UInt16 sizeBank;

            binFile.Read(bytes, 0, (int) binFileSize);

            if (bytes[0x3800] == 0x65 && bytes[0x3801] == 0x61 &&
                bytes[0x3802] == 0x70 && bytes[0x3803] == 0x69)
            {
                var eapiFile = File.OpenRead("eapi-am29f040-14");
                eapiFile.ReadByte();
                eapiFile.ReadByte();
                eapiFile.Read(bytes, 0x3800, (int)eapiFile.Length - 2);
            }

            UInt16 fileoffset = 0x2000;
            byte filebank = 1;
            foreach (var fileNode in from f in efcartNode.Descendants("file")
                                     select f)
            {
                var fileStream = File.OpenRead((string) fileNode.Attribute("name"));

                if (fileStream != null)
                {
                    foreach(var b in ASCIIEncoding.ASCII.GetBytes(((string)fileNode.Attribute("name") + new string('\0', 16)).Substring(0, 16)))
                    {
                        bytes[fileoffset++] = b;
                    }

                    bytes[fileoffset++] = Convert.ToByte(fileNode.Attribute("flags").Value);
                    bytes[fileoffset++] = filebank;
                    bytes[fileoffset++] = 0;
                    bytes[fileoffset++] = 0;
                    bytes[fileoffset++] = 0;
                    bytes[fileoffset++] = (byte)(fileStream.Length % 256);
                    bytes[fileoffset++] = (byte) ((fileStream.Length%65536)/256);
                    bytes[fileoffset++] = (byte)(fileStream.Length / 65536);

                    filebank+=(byte)((fileStream.Length / 0x4000) + 1);
                }
            }
            
            while (remaining > 0)
            {
                sizeBank = remaining > 0x2000 ? (UInt16)0x2000 : (UInt16)remaining;

                writeChipHeader(output, bank, (chip != 0 ? (UInt16)0xA000 : (UInt16)0x8000), sizeBank);
                bytesWritten += 16;

                var offset = bank*0x2000 + (chip*0x2000);

                Console.WriteLine("Writing offset: {0}   sizeBank: {1}", offset, sizeBank);

                output.Write(bytes, offset, sizeBank);
                bytesWritten += sizeBank;

                if (chip != 0)
                {
                    chip = 0;
                    ++bank;
                }
                else
                {
                    chip = 1;
                }

                remaining -= sizeBank;
            }

            foreach (var fileNode in from f in efcartNode.Descendants("file")
                                     select f)
            {
                if (File.Exists(fileNode.Attribute("name").Value))
                {
                    var fileStream = File.OpenRead((string)fileNode.Attribute("name"));
                    bytes = new byte[0xffffff];
                    remaining = (UInt32)fileStream.Read(bytes, 0, bytes.Length);
                    var padding = 0x4000 - (remaining%0x4000);
                    if(padding < 0x4000)
                        remaining += padding;
                    var counter = 0;
                    var banks = remaining/0x2000 + 1;
                    if (banks % 2 != 0) banks++;

                    Console.WriteLine("{0}: {1} banks starting at {2}", fileNode.Attribute("name"), banks, bytesWritten);

                    while (remaining > 0)
                    {
                        sizeBank = remaining > 0x2000 ? (UInt16)0x2000 : (UInt16)remaining;
                        
                        writeChipHeader(output, bank, (chip == 1 ? (UInt16)0xA000 : (UInt16)0x8000), 0x2000);
                        bytesWritten += 16;

                        output.Write(bytes, counter, 0x2000);
                        counter += 0x2000;
                        bytesWritten += 0x2000;

                        if (chip != 0)
                        {
                            chip = 0;
                            ++bank;
                        }
                        else
                        {
                            chip = 1;
                        }

                        remaining -= sizeBank;
                        --banks;
                    }
                }
            }

            output.Close();
            Console.WriteLine("Wrote {0} bytes.", bytesWritten);

            return 0;
        }

        static void writeChipHeader(FileStream output, byte bank, UInt16 address, UInt16 sizeBank)
        {
            output.Write(ASCIIEncoding.ASCII.GetBytes("CHIP"), 0, 4);

            output.WriteByte(0);
            output.WriteByte(0);
            output.WriteByte(Convert.ToByte((sizeBank + 0x10) / 256));
            output.WriteByte(Convert.ToByte((sizeBank + 0x10) % 256));

            output.WriteByte(0);
            output.WriteByte(2);

            output.WriteByte(Convert.ToByte(bank / 256));
            output.WriteByte(Convert.ToByte(bank % 256));

            output.WriteByte(Convert.ToByte(address / 256));
            output.WriteByte(Convert.ToByte(address % 256));

            output.WriteByte(Convert.ToByte(sizeBank / 256));
            output.WriteByte(Convert.ToByte(sizeBank % 256));
        }

        static void writeHeader(FileStream output, byte[] bytes, string cartName)
        {
            var cartHeaderBytes = ASCIIEncoding.ASCII.GetBytes("C64 CARTRIDGE                 ".Substring(0, 16));

            output.Write(cartHeaderBytes, 0, 16);

            bytes[0] = 0;
            bytes[1] = 0;
            bytes[2] = 0;
            bytes[3] = 0x40;

            output.Write(bytes, 0, 4);

            bytes[0] = 0x01;
            bytes[1] = 0;

            output.Write(bytes, 0, 2);

            bytes[0] = 0;
            bytes[1] = 32;

            output.Write(bytes, 0, 2);

            bytes[0] = 1;
            bytes[1] = 0;

            bytes[2] = 0;
            bytes[3] = 0;
            bytes[4] = 0;
            bytes[5] = 0;
            bytes[6] = 0;
            bytes[7] = 0;

            output.Write(bytes, 0, 8);

            var cartNameBytes = ASCIIEncoding.ASCII.GetBytes((cartName + new string('\0', 32)).Substring(0, 32));

            output.Write(cartNameBytes, 0, 32);
            
        }
    }
}
